传输层

TCP/IP协议栈

概述

  • 传输层协议为运行在不同主机上的应用进程之间提供了逻辑通信
  • 传输层协议只运行于端系统中,也就是说传输层不运行在路由器和交换机中

主要功能

  1. 提供进程与进程之间的通信。
  2. 复用和分用(后边会详细解释的)
  3. 对收到的报文进行差错检测。

与网络层的关系

  • 在协议栈结构中,传输层运行于网络层之上

  • 传输层协议提供的服务 部分 受制于网络层,如:

    • 如底层网络层协议无法为报文提供时延和带宽保证,那么其上层传输层协议也做不到

    • 但是即使底层网络层协议不可靠,不安全,运行于其上的传输层协议仍然可以做到可靠安全传输

      例如IP协议是不可靠不安全的,但是我们通过TCP可以做到可靠的安全传输

寻址和端口

  • 复用:应用层所有的应用进程都可以通过传输层再传输到网络层。
  • 分用:传输层从网络层收到数据后交付指明的应用进程。
  • 逻辑端口/软件端口 : 端口是传输层的SAP,标识主机中的应用进程
  • 端口号只有本地意义,在因特网中不同计算机的相同端口是没有联系的。
  • 端口号长度为16bit,能表示65536个不同的端口号。

常用的端口号

协议/服务名称 端口号 简介
ftp 20、21 File Transfer Protocol 文件传输协议,20用于连接,21用于传输
ssh 22 Secure Shell 安全外壳协议,专为远程登录会话和其他网络服务提供安全性的协议
http 80 Hyper Text Transfer Protocol 超文本传输协议,用于网页浏览
DNS 53 Domain Name System 域名系统,域名解析
https 443 Hypertext Transfer Protocol Secure 超文本传输安全协议,用于安全浏览网页
www代理服务 8080 Apache Tomcat web server,进行网页浏览
smtp 25 Simple Mail Transfer Protocol 简单邮件传输协议
telnet 23 不安全的文本传送
pop3 110 Post Office Protocol

UDP(User Datagram Protocol)

  • UDP在IP数据报服务之上添加了复用分用差错检测的功能。

UDP特点

  1. UDP是无连接的,减少开销和发送数据之前的时延
  2. UDP使用最大努力交付,即不保证可靠交付
  3. UDP是面向报文的,适合一次性传输少量数据的网络应用。
  4. UDP无拥塞控制,适合很多实时应用。
  5. UDP首部开销小,8Byte,TCP 为20Byte。
image-20210830143456473

UDP的首部格式

UDP头部格式

UDP校验

UDP差错检测的常用方法是采用校验和机制,即在发送端计算校验和,将其添加到UDP数据报的头部,

接收端在接收到数据报后重新计算校验和,将其与UDP头部中的校验和进行比较以判断数据是否发生了差错

校验运行步骤

  1. 发送端在发送UDP数据报之前,先计算数据的校验和。计算方法通常是将UDP数据报中所有数据字节相加,然后对结果取反,得到校验和值。
  2. 将计算得到的校验和值添加到UDP数据报的头部,覆盖原来的校验和字段。
  3. 接收端在接收到UDP数据报后,同样按照上述方法计算校验和值。
  4. 接收端将计算得到的校验和值与UDP头部中的校验和进行比较,如果两者相等,则认为数据没有出现差错,可以继续处理;否则,认为数据发生了差错,需要进行相应的处理,例如丢弃该数据包或者请求发送端重新传输数据。

注意

尽管UDP提供标头和有效负载的完整性验证(通过校验和),但它不保证向上层协议提供消息传递

并且UDP层在发送后不会保留UDP 消息的状态。因此,UDP有时被称为不可靠的数据报协议。如果需要传输可靠性,则必须在用户应用程序中实现。

UDP的校验和机制并不能完全保证数据的可靠性,因为校验和只能检测出一部分差错,例如奇偶校验错、位错等。

而对于一些其他的差错,例如漏字、重复等,校验和并不能有效检测出来。因此,在应用层进行数据传输时,需要根据实际情况选择合适的差错检测和纠正机制,以保证数据的可靠性。

TCP(Transmission Control Protocol)

TCP特点

  1. 可靠性:TCP协议提供可靠的数据传输服务,通过确认、重传、拥塞控制等技术保证数据的可靠性。
  2. 面向连接:TCP协议是一种面向连接的协议,通信双方在进行数据传输之前需要先建立连接,数据传输完成后需要进行连接的释放。这种连接方式可以保证数据的可靠性和完整性。
  3. 流量控制:TCP协议通过滑动窗口机制实现流量控制,可以根据接收方的处理能力和可用缓存空间来控制发送方的数据传输速率,从而避免过多的数据发送导致网络拥塞。
  4. 拥塞控制:TCP协议通过拥塞窗口和慢启动等机制实现拥塞控制,可以动态调整数据传输的速率,从而避免网络拥塞和数据包丢失。
  5. 面向字节流:TCP协议是一种面向字节流的协议,将应用层的数据视为一系列无结构的字节流,可以实现可靠的数据传输和精细的流量控制。
  6. 双向传输:TCP协议支持全双工的数据传输,即通信双方可以同时发送和接收数据,数据传输的方向可以随时改变。
  7. 高效性:TCP协议通过各种优化技术(如快速重传、快速恢复、Nagle算法等)提高了数据传输的效率,可以在保证可靠性的前提下尽可能地提高数据传输速率。

面向字节流发送图示

TCP发送缓存和接收缓存

TCP首部

TCP 报文段的首部格式

首部固定部分各字段的意义如下:

(1)源端口和目的端口 各占 2 个字节,分别写入源端口号和目的端口号。 TCP 的分用功能是通过端口实现的。

(2)序列号(seq : Sequence Number) 占 4 字节。序号范围是[0, 232−1 ],共 232 (即 4 294 967 296)个序号。序号增加到 232−1 后,下一个序号就又回到 0。也就是说,序号使用 mod 232 运算。 TCP 是面向字节流的。在一个 TCP 连接中传送的字节流中的每一个字节都按顺序编号。整个要传送的字节流的起始序号必须在连接建立时设置。首部中的序号字段值则指的是本报文段所发送的数据的第一个字节的序号。例如,一报文段的序号字段值是 301,而携带的数据共有 100字节。这就表明:本报文段的数据的第一个字节的序号是 301,最后一个字节的序号是400。显然,下一个报文段(如果还有的话)的数据序号应当从 401 开始,即下一个报文段的序号字段值应为 401。这个字段的名称也叫做“报文段序号”。

(3)确认号() 占 4 字节,是期望收到对方下一个报文段的第一个数据字节的序号。例如, B 正确收到了 A 发送过来的一个报文段,其序号字段值是 501,而数据长度是 200 字节(序号 501 ~ 700),这表明 B 正确收到了 A 发送的到序号 700 为止的数据。因此, B 期望收到 A 的下一个数据序号是 701,于是 B 在发送给 A 的确认报文段中把确认号置为 701。现在的确认号不是 501,也不是 700,而是 701。

总之,应当记住:若确认号 = N,则表明:到序号 N – 1 为止的所有数据都已正确收到。

由于序号字段有 32 位长,可对 4 GB(即 4 千兆字节)的数据进行编号。在一般情况下可保证当序号重复使用时,旧序号的数据早已通过网络到达终点了。

(4)数据偏移 占 4 位,它指出 TCP 报文段的数据起始处距离 TCP 报文段的起始处有多远。这个字段实际上是指出 TCP 报文段的首部长度。由于首部中还有长度不确定的选项字段,因此数据偏移字段是必要的。但应注意,“数据偏移”的单位是 32 位字(即以 4 字节长的字为计算单位)。由于 4 位二进制数能够表示的最大十进制数字是 15,因此数据偏移的最大值是 60 字节,这也是 TCP 首部的最大长度(即选项长度不能超过 40 字节)。

(5)保留 占 6 位,保留为今后使用,但目前应置为 0。

下面有 6 个控制位,用来说明本报文段的性质,它们的意义见下面的(6)~(11)。

(6)紧急 URG (URGent) 当 URG = 1 时,表明紧急指针字段有效。它告诉系统此报文段中有紧急数据,应尽快传送(相当于高优先级的数据),而不要按原来的排队顺序来传送。例如,已经发送了很长的一个程序要在远地的主机上运行。但后来发现了一些问题,需要取消该程序的运行。因此用户从键盘发出中断命令( Control + C)。如果不使用紧急数据,那么这两个字符将存储在接收 TCP 的缓存末尾。只有在所有的数据被处理完毕后这两个字符才被交付接收方的应用进程。这样做就浪费了许多时间。

当 URG 置 1 时,发送应用进程就告诉发送方的 TCP 有紧急数据要传送。于是发送方TCP 就把紧急数据插入到本报文段数据的最前面,而在紧急数据后面的数据仍是普通数据。这时要与首部中紧急指针(Urgent Pointer)字段配合使用。

(7)确认 ACK (ACKnowledgment) 仅当 ACK = 1 时确认号字段才有效。当 ACK = 0时,确认号无效。 TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置 1。

(8)推送 PSH (PuSH) 当两个应用进程进行交互式的通信时,有时在一端的应用进程希望在键入一个命令后立即就能够收到对方的响应。在这种情况下, TCP 就可以使用推送(push)操作。这时,发送方 TCP 把 PSH 置 1,并立即创建一个报文段发送出去。接收方TCP 收到 PSH = 1 的报文段,就尽快地(即“推送”向前)交付接收应用进程,而不再等到整个缓存都填满了后再向上交付。

虽然应用程序可以选择推送操作,但推送操作很少使用。

(9)复位 RST (ReSet) 当 RST = 1 时,表明 TCP 连接中出现严重差错(如由于主机崩溃或其他原因),必须释放连接,然后再重新建立运输连接。 RST 置 1 还用来拒绝一个非法的报文段或拒绝打开一个连接。 RST 也可称为重建位或重置位。

(10)同步 SYN (Synchronization) 在连接建立时用来同步序号。当 SYN = 1 而 ACK= 0 时,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使SYN = 1 和 ACK = 1。因此, SYN 置为 1 就表示这是一个连接请求或连接接受报文。

(11)终止 FIN (FINis,意思是“完”、“终” ) 用来释放一个连接。当 FIN = 1 时,表明此报文段的发送方的数据已发送完毕,并要求释放运输连接。

(12)窗口 占 2 字节。窗口值是[0, 216 – 1]之间的整数。窗口指的是发送本报文段的一方的接收窗口(而不是自己的发送窗口)。窗口值告诉对方:从本报文段首部中的确认号算起,接收方目前允许对方发送的数据量(以字节为单位)。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。总之, 窗口值作为接收方让发送方设置其发送窗口的依据。

例如,发送了一个报文段,其确认号是 701,窗口字段是 1000。这就是告诉对方:“从701 号算起,我(即发送此报文段的一方)的接收缓存空间还可接收 1000 个字节数据(字节序号是 701 ~ 1700),你在给我发送数据时,必须考虑到这一点。”

总之,应当记住:窗口字段明确指出了现在允许对方发送的数据量。窗口值经常在动态变化着。

(13)检验和 占 2 字节。检验和字段检验的范围包括首部和数据这两部分。和 UDP用户数据报一样,在计算检验和时,要在 TCP 报文段的前面加上 12 字节的伪首部。伪首部的格式与图中 UDP 用户数据报的伪首部一样。但应把伪首部第 4 个字段中的 17 改为 6(TCP 的协议号是 6),把第 5 字段中的 UDP 长度改为 TCP 长度。接收方收到此报文段后,仍要加上这个伪首部来计算检验和。若使用 IPv6,则相应的伪首部也要改变。

(14)紧急指针 占 2 字节。紧急指针仅在 URG = 1 时才有意义,它指出本报文段中的紧急数据的字节数(紧急数据结束后就是普通数据)。因此,紧急指针指出了紧急数据的末尾在报文段中的位置。 当所有紧急数据都处理完时, TCP 就告诉应用程序恢复到正常操作。值得注意的是,即使窗口为零时也可发送紧急数据。

注意 : 确认号(ACK : Acknowledgment Number)与控制位中的ACK需要区分开

在TCP协议中,**确认号码(ACK)**是指接收方向发送方发送的TCP报文段进行确认时使用的一个字段,它携带了接收方期望下一次收到的数据报文段的序列号。当发送方收到ACK报文段时,它就知道接收方已经成功接收了之前发送的数据报文段,可以继续发送下一个数据报文段。

而TCP首部中的ACK控制位是一个单独的标志位,用于表示TCP报文段是否携带确认号码。当ACK标志位被设置为1时,表示这个TCP报文段携带了确认号码。当ACK标志位被设置为0时,则表示这个TCP报文段不含确认号码,它可能是一个数据报文段,也可能是一个控制报文段(如SYN、FIN等)。

  • 确认号码与ACK控制位的区别在于,确认号码是一个用于确认数据报文段的字段,而ACK控制位则是一个用于标记TCP报文段是否含有确认信息的标志位。确认号码可以被包含在携带ACK标志位的TCP报文段中,也可以单独被发送,而ACK标志位只是TCP报文段头部的一个控制标志

TCP连接

什么是TCP连接

RFC 793
Connections: The reliability and flow control mechanisms described above require that TCPs initialize and maintain certain status information for each data stream. The combination of this information, including sockets, sequence numbers, and window sizes, is called a connection.

连接:上述可靠性和流量控制机制要求TCP初始化并维护每个数据流的某些状态信息。这些信息的组合,包括套接字序列号窗口大小,称为连接。

**连接: **

  • Socket:由 IP 地址和端口号组成
  • 序列号:用来解决乱序问题等
  • 窗口大小:用来做流量控制

建立连接

假设运行在一台主机(客户)上的一个进程想与另一台主机(服务器)上的一个进程建立一条连接,客户应用进程首先通知客户TCP,他想建立一个与服务器上某个进程之间的连接,客户中的TCP会用以下步骤与服务器中的TCP建立一条TCP连接:

  • SYN 连接请求报文
  • seq 序号
  • ACK 确认连接
  • ack 确认号

TCP 报文段的首部格式

  1. 客户端TCP向服务器端TCP发送一个SYN报文段
  • SYN报文段:无应用层数据,标志字段中SYN比特置为1,序号字段由客户随机选择,该序号是本次TCP连接客户的初始序号(client_isn)
  1. 服务器的收到TCP SYN报文段,为该TCP连接分配TCP缓存和变量,并向客户回送SYNACK报文段
  • SYNACK报文段:无应用层数据,标志字段中SYN比特置为1,序号字段由客户随机选择,该序号是本次TCP连接服务器的初始序号(server_isn),确认号字段为client_isn+1。此时TCP连接已经建立。
  1. 客户收到SYNACK报文段,为该TCP分配缓存和变量,并向服务器发送对SYNACK的确认报文。
  • 该报文为一个普通报文,其标志字段中SYN比特为0,序号为client_isn+1,确认号为server_isn+1。该报文可携带应用 层数据

可以发现第三次握手是可以携带数据的,前两次握手是不可以携带数据的

抓包

接下来我们以HTTP协议访问NGINX服务器为例,详细地查看TCP三次握手的细节

为了避免访问互联网出现过多的数据导致信息检索难度骤增,这里使用主机与虚拟机进行HTTP通信来进行抓包分析

准备内容

  1. Vmware Workstation 16 虚拟机 : centos7.6 虚拟机IP地址为 192.168.159.134

  2. 使用docker创建Nginx服务器镜像,配置访问映射为80 : 80

    详细配置容器步骤请参考 docker常用容器部署命令总结 | dhx_'blog

    1
    2
    [root@localhost ~]# docker  ps | grep nginx
    72a091cb9889 nginx:1.10 "nginx -g 'daemon of…" 3 months ago Up 2 weeks 0.0.0.0:80->80/tcp, :::80->80/tcp, 443/tcp nginx
  3. Wire Shark抓包工具 : 官网 Wireshark · Go Deep

WireShark配置捕获Vmnet8

选择tcp.port==80

访问Nginx服务器的端口。

为了直接使用TCP进行网络通信,我们通过Java的socket API来发送TCP请求

1
2
3
4
5
public static void main(String[] args) throws IOException, InterruptedException {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("192.168.159.134",80));
sc.close();
}

抓包结果 :

可以看到三次握手的控制位与理论结果相同。

下图为第一次握手发送的内容,其中SYN被置为1

TCP报文段信息 从上到下依次对应上面给出的TCP首部的示意图。 TCP 报文段的首部格式

本次的TCP报文长度为 32 Byte , 包括20Byte的固定长度TCP首部以及12Byte的选项内容

对于第二次握手发送的内容,服务器响应ACK给我们的客户端,接着我们的客户端响应了ACK给NGINX服务器。

接着我们修改代码,传输hello world给NGINX服务器

java NIO相关知识请参考 Netty网络编程(1)JavaNIO-2 [Netty ]| dhx_'blog

1
2
3
4
5
6
public static void main(String[] args) throws IOException {
SocketChannel sc = SocketChannel.open();
sc.connect(new InetSocketAddress("192.168.159.134",80));
sc.write(StandardCharsets.UTF_8.encode("hello world"));
sc.close();
}

运行代码,再次抓包

这里我们着重看第九个TCP请求

查看TCP报文段内容,可以看到我们通过channel发送的hello world已经发送出去,同时NGINX服务器返回了ACK表示收到请求。

为什么是三次握手?不是两次、四次?

详细参考 TCP 三次握手与四次挥手面试题 | 小林coding (xiaolincoding.com)

保证双方都能收发数据

在TCP连接建立之前,双方之间互相不知道对方的状态。如果只有两次握手,那么只有客户端向服务器发送请求,服务器接收并确认请求,此时连接就建立了

但是,这种情况下,服务器并不能向客户端发送数据,因为此时服务器并不知道客户端是否能够正常接收数据

因此,需要在两次握手的基础上加上第三次握手,客户端向服务器发送确认请求,服务器收到确认后也向客户端发送确认,这样就可以保证双方都能正常收发数据

防止已失效的连接请求报文段产生错误

如果只有两次握手,那么客户端向服务器发送连接请求,但是由于网络延迟等原因,该请求未能及时到达服务器,而客户端又重新向服务器发送了一次连接请求。此时,第一次发送的连接请求报文段还未失效,如果服务器收到了这个已失效的请求,就会误认为客户端又想建立一条新的连接,从而产生错误。因此,需要通过第三次握手来避免这种错误的发生。

防止资源浪费

如果只有四次握手,那么在第四次握手之前,服务器已经向客户端发送了FIN(关闭连接)请求,但是由于网络延迟等原因,该请求未能及时到达客户端。

此时,客户端仍然可以向服务器发送数据,但是服务器已经关闭了连接,从而导致资源的浪费。因此,需要通过第三次握手来避免这种情况的发生。

释放连接

TCP 断开连接是通过四次挥手方式。

双方都可以主动断开连接,断开连接后主机中的「资源」将被释放

四次挥手的过程
  • 比如说上图中客户端打算关闭连接,此时会发送一个 TCP 首部 FIN 标志位被置为 1 的报文,也即 FIN 报文,之后客户端进入 FIN_WAIT_1 状态。
  • 服务端收到该报文后,就向客户端发送 ACK 应答报文,接着服务端进入 CLOSE_WAIT 状态。
  • 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。
  • 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。
  • 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态
  • 服务端收到了 ACK 应答报文后,就进入了 CLOSE 状态,至此服务端已经完成连接的关闭。
  • 客户端在经过 2MSL 一段时间后,自动进入 CLOSE 状态,至此客户端也完成连接的关闭。

在断开连接之前客户端和服务器都处于ESTABLISHED状态,双方都可以主动断开连接以客户端主动断开连接为优

以四次挥手的方式叙述:

第一次挥手:客户端打算断开连接,向服务器发送FIN报文(FIN标记位被设置为1,1表示为FIN,0表示不是),FIN报文中会指定一个序列号,之后客户端进入FIN_WAIT_1状态。

也就是客户端发出连接释放报文段(FIN报文),指定序列号seq = u,主动关闭TCP连接,等待服务器的确认。

第二次挥手:服务器收到连接释放报文段(FIN报文)后,就向客户端发送ACK应答报文,以客户端的FIN报文的序列号 seq+1 作为ACK应答报文段的确认序列号ack = seq+1 = u + 1。

接着服务器进入CLOSE_WAIT(等待关闭)状态,此时的TCP处于半关闭状态(下面会说什么是半关闭状态),客户端到服务器的连接释放。客户端收到来自服务器的ACK应答报文段后,进入FIN_WAIT_2状态。

第三次握手:服务器也打算断开连接,向客户端发送连接释放(FIN)报文段,之后服务器进入LASK_ACK(最后确认)状态,等待客户端的确认。

服务器的连接释放(FIN)报文段的FIN=1,ACK=1,序列号seq=m,确认序列号ack=u+1。

第四次握手:客户端收到来自服务器的连接释放(FIN)报文段后,会向服务器发送一个ACK应答报文段,以连接释放(FIN)报文段的确认序号 ack 作为ACK应答报文段的序列号 seq,以连接释放(FIN)报文段的序列号 seq+1作为确认序号ack。

之后客户端进入TIME_WAIT(时间等待)状态,服务器收到ACK应答报文段后,服务器就进入CLOSE(关闭)状态,到此服务器的连接已经完成关闭。

客户端处于TIME_WAIT状态时,此时的TCP还未释放掉,需要等待2MSL后,客户端才进入CLOSE状态。

你可以看到,每个方向都需要一个 FIN 和一个 ACK,因此通常被称为四次挥手

这里一点需要注意是:主动关闭连接的,才有 TIME_WAIT 状态。

抓包

依旧是上面的抓包信息来进行分析。

那么通过看完图你可能会感到疑惑,不是四次挥手吗??为什么上面的抓包只能看到三次??

首先,三次挥手是因为第二次和第三次合并了

四次挥手,我们知道是客户端和服务器之间交互的四个报文,FIN、ACK、FIN、ACK

此时我们需要想一想TCP连接的特点:TCP是全双工的,也就是双方实际上都会建立TCP连接,四次挥手本质上就是双方都关闭通往对方的连接

对于控制位FIN,他的真正用途是当本端没有数据发送给对方时,关闭从本端到对端的连接

收到本端FIN报文时,对方的接收通道就会关闭。

此时如果对方也没有数据需要发送给本端的时候,那么对方也会发送FIN给本端,用于关闭从对方到本端的连接这时候就可能出现ACK和FIN合在一起的情况

当然,如果这个时候对方还有数据需要发送给本端,那么就是正常的四次挥手

也就是我们说的第二次以及第三次挥手合并了,此时本端在收到对方的这个带有FIN 以及 ACK 的请求,只需要再次发送ACK,双方即可全部完成连接的关闭

TCP重传

我们对于网络需要有这样的认识 : 在错综复杂的网络,并不一定所有的数据能正常的数据传输,对于网络层的IP服务,既不能保证数据报的交付,也不保证数据报的按序交付,也不保证数据报的完整性。

TCP在IP的基础之上提供可靠数据传输,那么为了保证所有的数据报都能够到达,就需要一些重传机制。

重传的工作方式主要借助TCP头部中的**序列号(Seq)确认号(ACK)**来决定是否重传,主要有以下几种:

  • 超时重传
  • 快速重传
  • SACK
  • D-SACK

超时重传

发送方在发送数据时设置一个定时器,当超过指定时间后如果还没有收到接收方的ACK响应,就会重发数据包。

  • RTT(Round Trip Time):往返时延,也就是**数据包从发出去到收到对应 ACK 的时间。**RTT 是针对连接的,每一个连接都有各自独立的 RTT。
  • RTO(Retransmission Time Out):重传超时,也就是前面说的超时时间。

试想 ,如果RTO过长,假设发生数据包丢失,那么需要很长时间才能重传,效率低下

如果过短,如果数据包因为网络状况阻塞传输慢但没有丢失,这时也会触发重传,会导致网络更加阻塞,触发更多的重传。

因此如何设定RTO的时间对于超时重传的机制十分重要

如何设置RTO

RTO既不能过长也不能过短,略微大于RTT是最好的。

RTT会因为网络的变化而发生变化,所以在Linux系统中为了计算RTO,会对RTT进行两个采样:

  • 通过采样RTT时间,然后加权平均,算出一个平滑RTT值这个RTT值因网络状况不断变化
  • 采样RTT的波动范围,避免发现不了过大波动的情况。

RFC6289建议使用以下公式计算RTO:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 首次计算RTO,R1为第一次测量的RTT
SRTT = R1
DevRTT = R1/2
RTO = μ * SRTT + δ * DevRTT = μ * R1 + δ * (R1/2)

# 后续计算RTO,R2为新测量的RTT

# SRTT是平滑的RTT值
SRTT = SRTT + α * (RTT - SRTT) = R1 + α * (R2 - R1)

# DevRTT是平滑的RTT和当前的RTT之间的差值
DevRTT = (1 - β) * DevRTT + β * (|RTT-SRTT|)= (1 - β) * (R1/2) + β * (R2 - R1)

RTO = μ * SRTT + δ * DevRTT

上述表达式中,在linux中α = 0.125,β = 0.25,μ = 1,δ = 4,至于为啥是这些值,别问问就是前人大量的测试积累得出。

这个算法的整体思想就是结合平均值平均偏差来进行估算,通过调参得到不错的效果。详细参考「RFC6298」。

那么现在我们可以确定超时重传的时间了,超时重传的机制也很简单,

首先TCP会为每个数据包设定一个计时器,一旦超过 RTO 而没有收到 ACK,就重发该数据包。没收到 ACK 的数据包都会存在重传缓冲区里,等到 ACK 后,就从缓冲区里删除。

首先明确一点,对 TCP 来说,超时重传是相当重要的事件(RTO 往往大于两倍的 RTT,超时往往意味着拥塞),一旦发生这种情况,TCP 不仅会重传对应数据段,还会降低当前的数据发送速率因为TCP 会认为当前网络发生了拥塞

然而超时重传也存在着非常明显的问题:

比如上图中的传输过程,由于网络中的不可预知的原因,数据包5丢失数据包 6,7,8,9 都已经到达接收方,这个时候客户端就只能等服务器发送 ACK,

注意对于包 6,7,8,9,服务端会把这几个数据包保存在TCP缓存中(滑动窗口机制),并且对于这几个数据包,由于发送序列号的问题(数据包5在6789前面),服务端是不能对这几个的数据包回应ACK的,那么这个时候对于客户端来说,他完全不知道丢了几个包

TCP为了保证数据传输的可靠性,就会把5 6 7 8 9 这五个数据包全部进行重传,这对于网络来说是非常大的资源浪费

快速重传

TCP流量控制

4.2 TCP 重传、滑动窗口、流量控制、拥塞控制 | 小林coding (xiaolincoding.com)

  • 流量控制:让发送方慢点,要让接收方来得及接收。
  • TCP利用滑动窗口机制实现流量控制。

在通信过程中,接收方根据自己接收缓存的大小,动态地调整发送方的发送窗口大小,即接收窗口rwnd(接收方设置确认报文段的窗口字段来将rwnd通知给发送方),发送方的发送窗口取接收窗口rwnd和拥塞窗口cwnd的最小值。

TCP拥塞控制

  • 出现拥塞的条件
    对资源需求的总和>可用资源
  • 网络中有许多资源同时呈现供应不足→网络性能变坏→网络吞吐量将随输入负荷增大而下降
  • 拥塞控制
    防止过多的数据注入到网络中。全局性

拥塞控制和流量控制的区别

拥塞控制是全局性的。

流量控制是点对点的。

  • 在介绍拥塞控制算法之前,先设定几个前提条件,便于理解。

慢开始和拥塞避免

  • 刚开始进行指数增长,到达 ssthresh 之后,进行加法增长。
  • 遇到网络拥塞之后,降到初始值,重复之前的步骤。新的 ssthresh 设为 拥塞时窗口大小的一半。

快重传和快回复

  • 前面的步骤和之前一样。不同的是降低拥塞窗口的时机和大小不同。如图所示,==当收到3个重复的确认时,执行快重传算法,拥塞窗口降到原来的一般。==